package crontab

import (
	"fmt"
	"github.com/robfig/cron/v3"
	"gluenet/internal/glue-srvm/model"
	"gluenet/internal/glue-srvm/pkg/gluelog"
	"gluenet/internal/glue-srvm/service"
	"gluenet/internal/glue-srvm/storages"
	"gluenet/pkg/database/mysql"
	"gorm.io/gorm"
	"math/rand"
	"strconv"
	"time"
)

const (
	ChargeTypeDemand = iota + 1
)

const (
	UseType = iota + 1
)

func GetRandomString(l int) string {
	var result []byte
	str := "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
	bytes := []byte(str)
	r := rand.New(rand.NewSource(time.Now().UnixNano()))
	for i := 0; i < l; i++ {
		result = append(result, bytes[r.Intn(len(bytes))])
	}
	return string(result)
}

func CronMetering() {
	c := cron.New()
	spec := fmt.Sprintf("0 * * * *")
	_, err := c.AddFunc(spec, func() {
		gluelog.Infoln("开始计量计费")
		Metering()
	})
	if err != nil {
		gluelog.Errorln("扣费失败", "err:", err)
	}
	c.Start()
	gluelog.Infoln("计量计费任务开始")
}

func Metering() {
	// DB for query, tx for transaction
	var err error
	var tx *gorm.DB
	defer func() {
		if err != nil && tx != nil {
			tx.Rollback()
		}
	}()
	db := mysql.GetDB()
	// 1.Get all accounts
	var accounts []model.MeteringAccount
	accounts, err = storages.DefaultAccountStorage.FindAll(db, model.MeteringAccount{})
	if err != nil {
		gluelog.Errorln("获取所有账户信息失败", "err:", err)
		return
	}
	// 2.map[accountID][]machineID
	var accountToHosts []model.MeteringAccountToHost
	accountMachineMap := make(map[uint64][]uint64)
	accountBalanceMap := make(map[uint64]float64)
	accountBalanceMapConst := make(map[uint64]float64)
	for _, account := range accounts {
		accountToHosts, err = storages.DefaultAccountToHostStorage.FindAll(db, model.MeteringAccountToHost{AccountId: account.Id, Status: service.MachinePermissionStatusNormal})
		if err != nil {
			gluelog.Errorln("获取所有账户与机器关联信息失败", "err:", err)
			return
		}
		for _, accountToHost := range accountToHosts {
			accountMachineMap[account.Id] = append(accountMachineMap[account.Id], accountToHost.HostId)
			accountBalanceMap[account.Id] = account.Balance
			accountBalanceMapConst[account.Id] = account.Balance
		}
	}
	// 3.Billing rules map[accountID][money]
	accountExpenseMap := make(map[uint64][]model.MeteringExpense, 0)
	accountCostMap := make(map[uint64]float64)
	for _, v := range accountToHosts {
		// 3.1 Get machine information
		var machine *model.MaasMachine
		machine, err = storages.DefaultMachineStorage.FindOne(db, model.MaasMachine{Id: v.HostId})
		if err != nil {
			gluelog.Errorln("获取机器信息失败", "err:", err)
			return
		}
		// 3.2 Get Billing Rules
		typeMoneyMap := make(map[string]int)
		var meteringRules []model.Dict
		meteringRules, err = storages.DefaultDictStorage.FindAll(db, model.Dict{Category: "ChargeRule"})
		if err != nil {
			gluelog.Errorln("获取计费规则失败", "err:", err)
			return
		}
		for _, v := range meteringRules {
			money, _ := strconv.Atoi(v.KeyAlias)
			typeMoneyMap[v.Key] = money
		}
		var usage uint64
		var startTime time.Time
		var owedMoney float64
		var cost float64
		if time.Now().Unix()-int64(machine.CreatedAt.Second()) > 24*60*60 {
			usage = 3600
			startTime = time.Now().Add(-3600 * time.Second)
		} else {
			usage = uint64(time.Now().Unix() - int64(machine.CreatedAt.Second()))
			startTime = time.Now().Add(-time.Duration(usage) * time.Second)
		}
		usedTime := float64(usage) / 3600
		// Compute CPU usage
		cpuMoney := float64(machine.CpuCount) * float64(typeMoneyMap["CPU"]) * usedTime
		// Compute memory usage
		memoryMoney := float64(machine.Memory) / 1024 * float64(typeMoneyMap["Memory"]) * usedTime
		// Computational storage usage
		storageMoney := float64(machine.Storage) / 1000 * float64(typeMoneyMap["Storage"]) * usedTime
		if machine.PowerState == "on" {
			cost = cpuMoney + memoryMoney + storageMoney
		} else {
			cost = storageMoney
		}
		if accountBalanceMap[v.AccountId] > (cost) {
			owedMoney = 0
			accountBalanceMap[v.AccountId] -= cost
		} else {
			owedMoney = (cost) -
				accountBalanceMap[v.AccountId]
			accountBalanceMap[v.AccountId] = 0
		}
		accountExpenseMap[v.AccountId] = append(accountExpenseMap[v.AccountId], model.MeteringExpense{
			ExpenseId:            GetRandomString(12),
			ResourceName:         "VMHost",
			ResourceType:         service.MachineTypeVMHost,
			ChargeType:           ChargeTypeDemand,
			UseType:              UseType,
			Usage:                int(usage),
			ConsumptionStartTime: startTime,
			ConsumptionEndTime:   time.Now(),
			BillingTime:          time.Now(),
			ConsumptionAmount:    cost,
			AmountOwed:           owedMoney,
		})
		accountCostMap[v.AccountId] += cost
	}
	// 4.Traverse the accountID to deduct fees
	tx = db.Begin()
	// 4.1 Insert Expense Table
	for k, expenses := range accountExpenseMap {
		for _, expense := range expenses {
			id, err := storages.DefaultExpenseStorage.Insert(tx, expense)
			if err != nil {
				gluelog.Errorln("插入费用表失败", "err:", err)
				return
			}
			_, err = storages.DefaultAccountToExpenseStorage.Insert(tx, model.MeteringAccountToExpense{
				AccountId: k,
				ExpenseId: uint64(id),
			})
			if err != nil {
				gluelog.Errorln("插入账号与费用关联表失败", "err:", err)
				return
			}
		}
	}
	// 4.2 Update account table
	arrearsAccount := make([]uint64, 0)
	for k, v := range accountCostMap {
		var balance, arrears float64
		if accountBalanceMapConst[k] > v {
			balance = accountBalanceMapConst[k] - v
			arrears = 0
		} else {
			balance = 0
			arrears = v - accountBalanceMapConst[k]
			arrearsAccount = append(arrearsAccount, k)
		}
		account, err := storages.DefaultAccountStorage.FindOne(tx, model.MeteringAccount{Id: 1})
		if err != nil {
			gluelog.Errorln("查找账户表失败", "err:", err)
			return
		}
		account.Balance = balance
		account.Arrears = arrears
		err = storages.DefaultAccountStorage.Update(tx, model.MeteringAccount{Id: k}, *account)
		if err != nil {
			gluelog.Errorln("更新账户表失败", "err:", err)
			return
		}
	}
	// 4.3 In case of arrears, update the account and machine association table
	if len(arrearsAccount) > 0 {
		for _, v := range arrearsAccount {
			err = storages.DefaultAccountToHostStorage.Update(tx, model.MeteringAccountToHost{AccountId: v}, model.MeteringAccountToHost{
				Status: service.MachinePermissionStatusArrearage,
			})
			if err != nil {
				gluelog.Errorln("更新账户与机器关联表失败", "err:", err)
				return
			}
		}
	}
	tx.Commit()
	gluelog.Infoln(time.Now(), ":", "计费结束")
}
